Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

51.Gün - SwiftUI Networking: URLSession Kullanarak Uygulamayı Tamamlayalım

Bu bölümde URLSession’u kullanarak internet üzerinden veri gönderme ve almayı göreceğiz.

Geçerli Bir Address Olup Olmadığını Kontrol Etme #

Projemizdeki ikinci adım, kullanıcının adresini bir forma girmesine izin vermek olacak, ancak bunun bir parçası olarak bazı doğrulamalar ekleyeceğiz - yalnızca adresleri iyi görünüyorsa üçüncü adıma geçmek istiyoruz.

Bunu, daha önce oluşturduğumuz AddressView struct’a dört text field içerecek bir Form ekleyerek gerçekleştirebiliriz: name, street address, city ve zip. Daha sonra, kullanıcının son fiyatını göreceği ve ödeme yapabileceği bir sonraki ekrana geçmek için bir NavigationLink ekleyebiliriz.

CheckoutView adında yeni bir view ekleyerek başlayacağız. Bu view’ı address view push edecek. Şimdilik bunu bir yer tutucu olarak ele alacağız.

CheckoutView adında yeni bir SwiftUI view oluşturun ve ona AddressView’ın sahip olduğu Order property’nin ve preview’in aynısını verin.

struct CheckoutView: View {
    var order: Order

    var body: some View {
        Text("Hello, World!")
    }
}

#Preview {
    CheckoutView(order: Order())
}

Buna daha sonra döneceğiz, ancak önce AddressView’ı uygulayalım. Dediğim gibi, bunun Order nesnemizden dört property’ye bağlı dört text filed içeren bir forma ve kontrolü checkout view’e akataran bir NavigationLink’e sahip olması gerekiyor.

İlk olarak, teslimat ayrıntılarını saklamak için Order’da dört yeni property’ye ihtiyacımız var

var name = ""
var streetAddress = ""
var city = ""
var zip = ""

Şimdi AddressView’in mevcut body ‘sini bununla değiştirin;

Form {
    Section {
        TextField("Name", text: $order.name)
        TextField("Street Address", text: $order.streetAddress)
        TextField("City", text: $order.city)
        TextField("Zip", text: $order.zip)
    }

    Section {
        NavigationLink("Check out") {
            CheckoutView(order: order)
        }
    }
}
.navigationTitle("Delivery details")
.navigationBarTitleDisplayMode(.inline)

Gördüğünüz gibi, bu işlem order nesnemizi bir seviye daha derine, CheckoutView’a aktarır; bu da artık aynı veriye işaret eden üç view’ımız olduğu anlamına gelir.

Bu kod birçok hataya yol açacaktır, ancak bunları düzeltmek için sadece küçük bir değişiklik yeterlidir, order property’yi şu şekilde değiştirin.

@Bindable var order: Order

Daha önce, bu property’ler @Observable makrolarını kullanan sınıflar olsa bile Xcode’un yerel @State property’lerine bind olmasına nasıl izin verdiğini görmüştünüz. Bunun nedeni, @State property wrapper’ın bizim için otomatik olarak $ sözdizimi aracılığıyla eriştiğimiz two way binding oluşturmasıdır.

AddressView’de @State kullanmadık çünkü sınıfı burada oluşturmuyoruz, sadece başka bir yerden alıyoruz. Bu da SwiftUI’nin normalde kullandığımız two way binding’e erişimi olmadığı anlamına gelir ki bu da bir sorundur.

Şimdi, bu sınıfın @Observable makrosunu kullandığını biliyoruz, bu da SwiftUI’nin bu verileri değişiklikler için izleyebileceği anlamına geliyor. Dolayısıyla, @Bindable property wrapper’ın yaptığı şey bizim için eksik binding’leri oluşturmaktır. Yani local veri oluşturmak için @State kullanmak zorunda kalmadan @Observable makrosu ile çalışabilen two way binding üretir. Burada mükemmeldir ve gelecekteki projelerinizde çok kullanacaksınız.

Devam edin ve uygulamayı tekrar çalıştırın, çünkü tüm bunların neden önemli olduğunu görmenizi istiyorum. İlk ekrana biraz veri girin, ikinci ekrana biraz veri girin, sonra başa dönüp ilerlemeyi deneyin

Görmeniz gereken şey, hangi ekranda olursanız olun girdiğiniz tüm verilerin kayıtlı kaldığıdır. Evet, bu verilerimiz için bir sınıf kullanmanın doğal bir yan etkisidir, ancak uygulamamızda herhangi bir çalışma yapmak zorunda kalmadan kazandığımız bir özelliktir. Local state kullansaydık, girdiğimiz tüm adres ayrıntıları orijinal görünüme geri döndüğümüzde kaybolurdu.

Artık AddressView çalıştığına göre, bazı koşullar yerine getirilmediği sürece kullanıcının ödeme sayfasına ilerlemesini durdurmanın zamanı geldi. Hangi koşul? Buna karar vermek bize düşüyor. Dört text field’ın her biri için uzunluk kontrolleri yazabilsek de, bu genellikle insanları yanıltır. Bazı isimler çok kısa olabilir.

Bunun yerine, siparişimizin name, streetAddress, city ve zip property’lerinin boş olup olmadığını kontrol edeceğiz. Verilerimin içine bu tür karmaşık bir kontrol eklemeyi tercih ediyorum, bu da Order’a bunun gibi yeni bir hesaplanmış özellik eklememiz gerektiği anlamına geliyor.

var hasValidAddress: Bool {
    if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
        return false
    }

    return true
}

Artık bu koşulu SwiftUI’nin disabled() modifier’ı ile birlikte kullanabiliriz.

Bizim durumumuzda, kontrol etmek istediğimiz koşul az önce yazdığımız hasValidAddress computed property’dir. Eğer bu property false ise, NavigationLink’imizi içeren form bölümünün devre dışı bırakılması gerekir, çünkü kullanıcıların önce teslimat bilgilerini doldurmaları gerekir.

Dolayısıyla, bu modifier’ı AddressView’daki ikinci bölümün sonuna ekleyelim;

.disabled(order.hasValidAddress == false)

Kod şu şekilde görülmelidir.

Section {
    NavigationLink("Check out") {
        CheckoutView(order: order)
    }
}
.disabled(order.hasValidAddress == false)

Şimdi uygulamayı çalıştırırsanız, devam etmek için dört text field’da da en az bir karakter içermesi gerektiğini göreceksiniz. Daha da iyisi, SwiftUI koşul doğru olmadığında butonu otomatik olarak grileştiriyor ve kullanıcıya etkileşimli olup olmadığı konusunda net bir geri bildirim veriyor.

Checkout için Hazırlık #

Uygulamamızdaki son ekran CheckoutView’dir ve iki bölümden oluşur. İlk yarı, sizin için çok az gerçek zorluk sağlayacak olan temel kullanıcı arayüzüdür, ancak ikinci yarı tamamen yenidir. Order sınıfımızı JSON’a encode etmemiz, internet üzerinden göndermemiz ve bir yanıt almamız gerekir.

Encode ve aktarma (transferring) işinin tamamına yakında bakacağız, ancak önce kolay kısmı ele alalım: CheckoutView’e bir kullanıcı arayüzü vermek. Daha spesifik olarak, bir resim, siparişlerinin toplam fiyatı ve network oluşturmayı başlatmak için bir Place Order butonu içeren bir ScrollView oluşturacağız.

Görüntü için sunucuma AsyncImage ile uzaktan alacağımız bir cupcake görüntüsü yükledim. Bunu uygulamada saklayabiliriz, ancak uzak bir görüntüye sahip olmak, mevsimsel alternatifler ve promosyonlar için dinamik olarak değiştirebileceğimiz anlamına gelir.

Sipariş maliyetine gelince, aslında verilemizde keklerimiz için herhangi bir fiyatlandırma yok, bu yüzden sadece bir tane icat edebiliriz. Kullanacağımız fiyatlandırma aşağıdaki gibidir.

  • Kek başına 2 dolarlık bir taban fiyat var.
  • Daha karmaşık kekler için ücrete biraz ekleme yapacağız.
  • Ekstra krema kek başına 1 dolar tutacaktır.
  • Süsleme eklemek kek başına 50 sent daha tutacaktır.

Tüm bu mantığı Order için aşağıdaki gibi yeni bir computed property’de toplayabiliriz.

var cost: Decimal {
    // $2 per cake
    var cost = Decimal(quantity) * 2

    // complicated cakes cost more
    cost += Decimal(type) / 2

    // $1/cake for extra frosting
    if extraFrosting {
        cost += Decimal(quantity)
    }

    // $0.50/cake for sprinkles
    if addSprinkles {
        cost += Decimal(quantity) / 2
    }

    return cost
}

Gerçek view’ın kendisi basittir: dikey bir ScrollView içinde bir VStack, ardından görüntümüz, maliyet metni ve sipariş vermek için buton kullanacağız.

Butonun action kısmına döneceğiz, önce temel layout’u tamamlayalım. CheckoutView’in mevcut body’sini bununla değiştirelim;

ScrollView {
    VStack {
        AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"), scale: 3) { image in
                image
                    .resizable()
                    .scaledToFit()
        } placeholder: {
            ProgressView()
        }
        .frame(height: 233)

        Text("Your total is \(order.cost, format: .currency(code: "USD"))")
            .font(.title)

        Button("Place Order", action: { })
            .padding()
    }
}
.navigationTitle("Check out")
.navigationBarTitleDisplayMode(.inline)

Bunların hepsi artık sizin için eski olmalı ancak bu ekranla işimiz bitmeden önce size buraya ekleyebileceğimiz küçük ama kullanışlı bir SwiftUI modifier göstermek istiyorum: scrollBounceBehavior()

Scroll view kullanmak, kullanıcının etkinleştirdiği Dinamik Tip boyutu ne olursa olsun layout’un harika çalışmasını sağlamanın güzel bir yoludur, ancak küçük bir sıkıntı yaratır: view’ler tek bir ekrana tam olarak sığdığında, kullanıcı üzerinde yukarı ve aşağı hareket ettiğinde yine de biraz zıplar.

scrollBounceBehavior() modifier’i, kaydırılacak bir şey olmadığında bu zıplamayı devre dışı bırakmamıza yardımcı olur. Bunu .navigationBarTitleDisplayMode(.inline) ’ın altına ekleyin.

.scrollBounceBehavior(.basedOnSize)

Bu şekilde, gerçekten kayan içeriğimiz olduğunda güzel bir kaydırma zıplaması elde edeceğiz, akdi takdirde scroll view orada yokmuş gibi davranır.

Bu bölümüde bitirdiğimize göre son kısım olan network oluşturmayı inceleyebiliriz.

İnternet Üzerinden Veri Gönderme ve Alma #

iOS, networkleri yönetmek için bazı harika işlevlerle birlikte gelir, özellikle URLSession sınıfı veri göndermeyi ve almayı şaşırtıcı derecede kolaylaştırır. Swift nesnelerini JSON’a ve JSON’dan dönüştürmeyi Codable ile birleştirirsek, verilerin tam olarak nasıl gönderilmesi gerektiğini yapılandırmak için yeni bir URLRequest struct kullanabiliriz, yaklaşık 20 satır kodla harika şeyler başarabiliriz.

İlk olarak, Place Order butonumuzdan çağırabileceğimiz bir method oluşturalım, bunu CheckoutView’e ekleyin;

func placeOrder() async {
}

Tıpkı URLSession kullanarak veri indirirken olduğu gibi, yükleme de asynchronous olarak yapılır.

Şimdi Place Order butonunu şu şekilde değiştirin;

Button("Place Order", action: placeOrder)
    .padding()

Bu kod çalışmayacaktır ve Swift bunun nedenini oldukça açık bir şekilde ortaya koyacaktır: asynchronous’u desteklemeyen bir fonksiyondan asynchronous bir fonksiyon çağırılmaktadır. Bunun anlamı butonumuzun action’ı hemen çalıştırabilmeyi beklediği ve asynchronous bir şeyi nasıl bekleyeceğini anlamadığıdır. await placeOrder() yazsak bile çalışmaz, çünkü buton beklemek istemez.

Daha önce onAppear() fonksiyonunun bu asenkron fonksiyonlarla çalışmadığından ve bunun yerine task() modifier’ını kullanamamız gerektiğinden bahsetmiştim. Burada sadece modifier eklemek yerine bir action yürüttüğümüz için bu bir seçenek değil, ancak Swift bir alternatif sunuyor: hiç yoktan yeni bir görev oluşturabiliriz ve tıpkı task() modifier gibi bu da istediğimiz her türlü asynchronous kodu çalıştıracaktır.

Aslında tek yapmamız gereken await çağrımızı aşağıdaki gibi bir görevin içine yerleştirmektir.

Button("Place Order") {
    Task {
        await placeOrder()
    }
}

Ve şimdi her şey hazır bu kod placeOrder() methodunu asynchronous olarak çağıracak. Tabi ki, bu fonksiyon aslında henüz hiçbir şey yapmıyor, bu yüzden şimdi bunu düzeltelim.

placeOrder() içinde üç şey yapmamız gerekiyor;

  1. Mevcut sipariş nesnemizi gönderebilecek bazı JSON verilerine dönüştürün.
  2. Swift’e bu verileri bir network çağrısı üzerinden nasıl göndereceğini söyleyin.
  3. Bu isteği çalıştırın ve yanıtı işleyin.

Bunlardan ilki basittir, bu yüzden onunla başlayalım. Bu kodu placeOrder()’a ekleyerek siparişimizi arşivlemek için JSONEncoder’ı kullanacağız;

guard let encoded = try? JSONEncoder().encode(order) else {
    print("Failed to encode order")
    return
}

Order sınıfı Codable protokolüne uymadığı için bu kod henüz çalışmayacaktır. Yine de bu kolay bir değişikliktir, sınıf tanımını şu şekilde değiştirelim;

class Order: Codable {

İkinci adım, URLRequest adında yeni bir tür kullanmaktır. Bu tür bize, istek türü, kullanıcı verileri gibi ekstra bilgiler eklemek için seçenekler sunan bir URL gibidir.

Sunucunun doğru şekilde işleyebilmesi için verileri çok özel bir şekilde eklememiz gerekiyor, bu da siparişimizin ötesinde iki ekstra veri sağlamamız gerektiği anlamına geliyor.

  1. Bir isteğin HTTP methodu, verilerin nasıl gönderileceğini belirler. Birkaç HTTP methodu vardır, ancak pratikte GET (”Veri okumak istiyorum”) ve POST (”Veri yazmak istiyorum”) çok kullanılır. Biz burada veri yazmak istiyoruz, bu yüzden POST kullanacağız.
  2. Bir isteğin içerik türü, ne tür bir veri gönderildiğini belirler ve bu da sunucunun verilerimizi ele alma şeklini etkiler. Bu, başlangıçta e-postalarda ek göndermek için yapılmış olan MIME türü olarak adlandırılan bir türde belirtilir e son derece spesifik birkaç bin seçeneği vardır.

Bu nedenle, placeOrder() için bir sonraki kod, bir URLRequest nesnesi oluşturmak ve ardından bir HTTP POST isteği kullanarak JSON verilerini gönderecek şekilde yapılandırmak olacaktır. Daha sonra bunu URLSession kullanarak verilerimizi yüklemek ve geri gelenleri işlemek için kullanabiliriz.

Tabiki asıl soru isteğimizi nereye göndereceğimizdir. https://reqres.in adlı gerçekten yararlı bir web sitesini kullanacağız, istediğimiz herhangi bir veriyi göndermemize izin veriyor ve otomatik olarak geri gönderiyor. Bu, network kodunun prototipini oluşturmanın harika bir yoludur, çünkü gönderdiğiniz her şeyden gerçek verileri geri alırsınız.

Bu kodu şimdi placeOrder() methoduna ekleyin;

let url = URL(string: "https://reqres.in/api/cupcakes")!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"

İlk satır URL(string:) initializer için bir force unwrap içeriyor, bu da “bu optional bir URL döndürür, ancak optional olmaması için zorlayın” anlamına geliyor. String’lerden URL oluşturmak, bazı anlamsız ifadeler ekleme ihtimalimiz dolayısyla başarısız olabilir, ancak burada URL’yi elle yazdım, böylece her zaman doğru olacağını görebiliyorum.

Bu noktada, URLSession.shared.upload() adlı yeni bir yöntem var ve az önce yaptığımız URL request’i kullanarak yapacağımız network isteğimizi yapmaya hazırız. Şimdi devam edin ve bunu placeOrder()’a ekleyin;

do {
    let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
    // handle the result    
} catch {
    print("Checkout failed: \(error.localizedDescription)")
}

Şimdi önemli işe gelelim: Her şeyin doğru çalıştığı zamanlar için isteğimizin sonucunu okumamız gerekiyor. Bir şeyler ters giderse belki de internet bağlantısı olmadığı için o zaman catch bloğumuz çalıştırılacaktır, bu yüzden burada bunun için endişelenmenize gerek yok.

ReqRes.in kullandığımız için, aslında gönderdiğimiz siparişin aynısını geri alacağız, bu da bunu JSON’da bir nesneye dönüştürmek için JSONDecoder’ı kullanabileceğimiz anlamına geliyor.

Her şeyin doğru çalıştığını doğrulamak için siparişimizin bazı ayrıntılarını içeren bir uyarı göstereceğiz, ancak ReqRes.in’den geri aldığımız decoded Order’ı kullanacağız. Evet bu bizim gönderdiğimizle aynı olmalı, eğer değilse encode’de bir hata yapmışız demektir.

Bir uyarının gösterilmesi, mesajı ve görünür olup olmadığını saklamak için property’ler gerektirir, bu nedenle lütfen bu iki yeni property’yi CheckoutView’e şimdi ekleyin;

@State private var confirmationMessage = ""
@State private var showingConfirmation = false

Ayrıca bu Boolean’ı izlemek ve doğru olduğu anda bir uyarı göstermek için bir alert() modifier eklememiz gerekir. Bu modifier’ı CheckoutView’deki navigation title altına ekleyin;

.alert("Thank you!", isPresented: $showingConfirmation) {
    Button("OK") { }
} message: {
    Text(confirmationMessage)
}

Ve şimdi network kodumuzu tamamlayabiliriz: geri gelen verilerin decode edeceğiz, confirmation message property’yi ayarlamak için kullanacağız, ardından uyarının görünmesi için showingConfirmation öğesini true olarak ayarlayacağız. Decode işlemi başarısız olursa yani sunucu herhangi bir nedenle order olmayan bir şey gönderirse bir hata mesajı yazdıracağız.

Bu son kodu placeOrder() methoduna ekleyin ve //handle the result yorumunu değiştirin.

let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
confirmationMessage = "Your order for \(decodedOrder.quantity)x \(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way!"
showingConfirmation = true

Şimdi çalıştırmayı denerseniz, tam olarak istediğiniz kekleri seçebilmeli, teslimat bilgilerinizi girebilmeli ve ardından bir uyarı görmek için Place Order butonuna basabilmelisiniz, her şey güzel çalışıyor.

Completed App

Yine de işimiz tam olarak bitmedi, çünkü şu anda ağımızın küçük ama görünmez bir sorunu var. Bunun ne olduğunu görmek için sizi Xcode ile küçük bir debugging işlemi ile tanıştırmak istiyorum. Uygulamamızı duraklatacağız, böylece belirli bir değeri inceleyebileceğiz.

İlk olarak let url = URL... satırının yanındaki satır numarasına tıklayın. Orada mavi bir ok görünmelidir, bu Xcode’un oraya bir kesme noktası yerleştirdiğimizi söyleme şeklidir. Bu Xcode’a o satıra ulaşıldığında yürütmeyi duraklatmasını söyler, böylece tüm verilemizi kurcalayabiliriz.

xcode breakpoint

Şimdi devam edin ve uygulamayı tekrar çalıştırın, sipariş verilerini girin ve ardından sipariş verin. Her şey yolunda gittiğinde uygulamanız duraklamalı, Xcode öne gelmeli ve çalıştırmak üzere olduğu için bu kod satırı vurgulanmalıdır.

Her şey yolunda giderse, Xcode penceresinin sağ lat kısmında Xocde’un hata ayıklama konsolunu görmelisiniz- normalde Apple’ın tüm dahili günlük mesajlarının göründüğü yerdir, ancak şu anda “(lldb)” yazmalıdır. LLDB, Xcode’un hata ayıklayıcısının adıdır ve verilerimizi keşfetmek için burada komutlar çalıştırabiliriz.

Orada şu komutu çalıştırmanızı istiyorum :  p String(decoding: encoded, as: UTF8.self) Bu, kodlanmış verilerimizi tekrar bir string’e dönüştürür ve yazdırır. Bize @Observable makrosu tarafından sağlanan observation registrar ile birlikte çok sayıda altı çizili değişken adı olduğunu görmelisiniz.

Xcode lldb debug underscore variables

Kodumuz aslında bunu önemsemiyor, çünkü tüm property’leri altı çizili isimlerle gönderiyoruz, ReqRes.in sunucusunu bunları bize aynı isimlerle geri gönderiyor ve biz de bunları altı çizili property’lere geri decode ediyoruz. Ancak gerçek bir sunucu ile çalışırken bu isimler önemlidir. Yani @Observable makrosu tarafından üretilen garip versiyonlar yerine gerçek isimleri göndermemiz gerekir.

Bu, Order sınıfı için bazı özel kodlama anahtarları oluşturmamız gerektiği anlamına gelir. Bu, özellikle bunun gibi birkaç property’yi kaydetmek ve yüklemek istediğimiz sınıflar için oldukça sıkıcıdır, ancak ağımızın düzgün bir şekilde yapıldığından emin olmanın en iyi yoludur.

Order sınıfını açın ve bu iç içe enumu buraya ekleyin.

enum CodingKeys: String, CodingKey {
    case _type = "type"
    case _quantity = "quantity"
    case _specialRequestEnabled = "specialRequestEnabled"
    case _extraFrosting = "extraFrosting"
    case _addSprinkles = "addSprinkles"
    case _name = "name"
    case _city = "city"
    case _streetAddress = "streetAddress"
    case _zip = "zip"
}

Kodu tekrar çalıştırırsanız, yukarı imleç tuşuna ve return tuşuna basarak p komutunu tekrar çalıştırabileceğinizi ve bu kez gönderilen ve alınan verilerin çok daha temiz olduğunu göreceksiniz.

Xcode lldb debug no underscore variables

Bu son kod ile birlikte hem ağımız hem de uygulamamız tamamlanmış oldu.


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 51 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.